package com.m4f.cityclient.oauth.consumer;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.Date;
import java.util.Enumeration;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2RefreshToken;
import org.springframework.security.oauth2.consumer.token.OAuth2ClientTokenServices;
import org.springframework.security.oauth2.consumer.webserver.WebServerProfileResourceDetails;
import org.springframework.security.web.PortResolverImpl;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.UrlUtils;

import com.google.api.client.auth.oauth2.AccessTokenResponse;
import com.google.api.client.auth.oauth2.AuthorizationRequestUrl;
import com.google.api.client.auth.oauth2.AuthorizationResponse;
import com.google.api.client.auth.oauth2.AccessTokenRequest.AuthorizationCodeGrant;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.jackson.JacksonFactory;

/**
 * @author David Basoko
 */
public class M4FOAuth2AuthenticationFilter extends AbstractAuthenticationProcessingFilter {
	private static final Logger LOGGER = Logger.getLogger(DataStoreOAuth2ClientTokenServices.class.getName());
	
	public static final String DEFAULT_URL = "/m4fauth";
	
	protected WebServerProfileResourceDetails resource;
	private OAuth2ClientTokenServices tokenServices;
	
	//private M4FUserApiService userApiService; // TODO try to use this service instead of doing this step by step here.
	
	public M4FOAuth2AuthenticationFilter() {
		super(DEFAULT_URL);
	}

	
	@Override
	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
		/*if(request.getParameter("error") != null && !("").equals(request.getParameter("error"))) {
			// TODO redirect to a JSP where show the error
			Writer writer = response.getWriter();
			String errorDescription = request.getParameter("error_description");
			
			//writer.write(errorDescription != null ? errorDescription : "");
			return null;
		}*/
		
		Authentication authenticationContext = SecurityContextHolder.getContext().getAuthentication();
		if(authenticationContext == null) {
			StringBuffer fullUrlBuf = request.getRequestURL();
		    if (request.getQueryString() != null) {
		      fullUrlBuf.append('?').append(request.getQueryString());
		    } else {
		    	AuthorizationRequestUrl result = new AuthorizationRequestUrl(resource.getUserAuthorizationUri()); // TODO
			    AuthorizationRequestUrl.ResponseType.CODE.set(result);
			    result.clientId = resource.getClientId();
			    result.redirectUri = this.calculateCurrentUri(request);
			  
			    result.scope = this.createScope(resource.getScope());
			    
			    response.sendRedirect(result.build());
			    return null;
		    }
		    
		    AuthorizationResponse authResponse = new AuthorizationResponse(fullUrlBuf.toString());
		    // check for user-denied error
		    if (authResponse.error != null) {
		    	// authorization denied...
		    	LOGGER.log(Level.INFO, "authorization denied!!");
		    } 
		      	// request access token using authResponse.code...	
		    	LOGGER.log(Level.INFO, "Request for an access token!!");
	            AuthorizationCodeGrant arequest = new AuthorizationCodeGrant();
	            arequest.authorizationServerUrl = resource.getAccessTokenUri();
	            arequest.clientId = resource.getClientId();
	            arequest.clientSecret = resource.getClientSecret();
	            arequest.code = authResponse.code;
	            arequest.redirectUri = this.calculateCurrentUri(request);
	            arequest.grantType = resource.getGrantType();
	            arequest.scope = this.createScope(resource.getScope());
	            arequest.useBasicAuthorization = true;
	            arequest.jsonFactory = new JacksonFactory();
	            arequest.transport = new NetHttpTransport();
	            AccessTokenResponse aresponse = null;
	            try {
	            	aresponse = arequest.execute().parseAs(AccessTokenResponse.class);
	            } catch(Exception e) {
	            	e.printStackTrace();
	            	throw new AuthenticationServiceException("Invalid request", e);
	            }
		        
	            // Creating a spring security oauth tokens and context because now there is an access token.
	            OAuth2AccessToken accessToken = new OAuth2AccessToken();
	            OAuth2RefreshToken refreshToken = new OAuth2RefreshToken();
	            accessToken.setValue(aresponse.accessToken);
	            accessToken.setExpiration(new Date(aresponse.expiresIn));
	            refreshToken.setValue(aresponse.refreshToken);
	            accessToken.setRefreshToken(refreshToken);
	            	            
	            Authentication authentication = getAuthenticationManager().authenticate(new M4FOAuth2AuthenticationToken(accessToken, resource.getId()));
	            if (authentication.isAuthenticated()) {
	            	setLastUsername(authentication.getName(), request);
	            	tokenServices.storeToken(authentication, resource, accessToken);
	    		}
	            
	    		return authentication;
		} else {
			// The is a security context established!
			return authenticationContext;
		}
	}
	
	private void setLastUsername(String username, HttpServletRequest request) {
		HttpSession session = request.getSession(false);
		if (session != null || getAllowSessionCreation()) {
			request.getSession().setAttribute(
				UsernamePasswordAuthenticationFilter.SPRING_SECURITY_LAST_USERNAME_KEY, username);
		}
	}
 
	 
	protected String calculateCurrentUri(HttpServletRequest request) throws UnsupportedEncodingException {
		StringBuilder queryBuilder = new StringBuilder();
		Enumeration paramNames = request.getParameterNames();
		while (paramNames.hasMoreElements()) {
			String name = (String) paramNames.nextElement();
			if (!"code".equals(name)) {
				String[] parameterValues = request.getParameterValues(name);
				if (parameterValues.length == 0) {
					queryBuilder.append(URLEncoder.encode(name, "UTF-8"));
				} else {
					for (int i = 0; i < parameterValues.length; i++) {
						String parameterValue = parameterValues[i];
						queryBuilder.append(URLEncoder.encode(name, "UTF-8")).append('=').append(URLEncoder.encode(parameterValue, "UTF-8"));
						if (i + 1 < parameterValues.length) {
							queryBuilder.append('&');
						}
					}
				}
			}

			if (paramNames.hasMoreElements()) {
				queryBuilder.append('&');
			}
		}
    
		return UrlUtils.buildFullRequestUrl(request.getScheme(), request.getServerName(), new PortResolverImpl().getServerPort(request), request.getRequestURI(), queryBuilder.length() > 0 ? queryBuilder.toString() : null);
	}

	public void setResource(WebServerProfileResourceDetails resource) {
		this.resource = resource;
	}

	public void setTokenServices(OAuth2ClientTokenServices tokenServices) {
		this.tokenServices = tokenServices;
	}
	
	private String createScope(List<String> scope) {
		StringBuilder sb = new StringBuilder();
		String newScope = "";
		
		if(scope != null) {
			for(String s : scope) {
				sb.append(s).append(",");
			}
			newScope = sb.toString();
		} 		
		
		if(newScope.endsWith(",")) newScope = newScope.substring(0, newScope.lastIndexOf(","));
		return newScope;
	}
	
}